package com.gallop.common.lock.aspect;

import com.gallop.common.CacheKeyAssembleHandler;
import com.gallop.common.cache.assemble.AssembleKeyParameter;
import com.gallop.common.lock.DistributeLocker;
import com.gallop.common.lock.DistributedConstants;
import com.gallop.common.lock.DistributedLock;
import com.gallop.common.lock.DistributedLockParam;
import com.gallop.common.lock.exception.AspectWrappedException;
import com.gallop.common.lock.exception.DistributedLockException;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

@Component
@Aspect
@Slf4j
public class DistributeLockAspect {

    private static final Set<Class<?>> ALLOWED_PARAM_KEY_TYPES = new HashSet<>(Arrays.asList(int.class, long.class, float.class,
            double.class, byte.class, boolean.class, char.class, String.class, Integer.class,
            Double.class, Float.class, Long.class, Byte.class, Character.class, Boolean.class));

    @Autowired
    private ApplicationContext applicationContext;

    @Pointcut("@annotation(com.gallop.common.lock.DistributedLock)")
    public void cut() {
    }

    @Around("cut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        Method method = getConcreteMethod(point);
        if (method == null) {
            return point.proceed();
        }
        DistributedLock annotation = method.getAnnotation(DistributedLock.class);
        if (annotation == null) {
            return point.proceed();
        }
        String key = appendLockKey(annotation, point, method);
        if (!StringUtils.hasLength(key)) {
            return point.proceed();
        }
        AtomicReference<Object> ret = new AtomicReference<>();
        DistributeLocker.getInstance().lock(key,
                annotation.maxWaitTimeUnit(), annotation.maxWait(),
                annotation.expireTimeUnit(), annotation.expireAfter(),
                annotation.fastFail(),
                () -> { // success
                    try {
                        ret.set(point.proceed());
                    } catch (Throwable e) {
                        log.error("Exception in lock context", e);
                        throw new AspectWrappedException(e);
                    }
                }, lockKey -> { // fail
                    throw new DistributedLockException(lockKey);
                }, ex -> { // error
                    throw new DistributedLockException(key, ex);
                }, Sets.newHashSet(annotation.exceptionThrows()));
        return ret.get();
    }

    private Method getConcreteMethod(ProceedingJoinPoint point) {
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        return methodSignature.getMethod();
    }

    private String appendLockKey(DistributedLock annotation, ProceedingJoinPoint point, Method method) {
        try {
            Object[] args = point.getArgs();
            List<Object> params = Lists.newArrayList();
            for (int i = 0; i < method.getParameters().length; i++) {
                Parameter p = method.getParameters()[i];
                if (p.isAnnotationPresent(DistributedLockParam.class)) {
                    if (!checkIsAllowedType(p.getType())) {
                        throw new IllegalArgumentException(String.format("invalid DistributedLockParam: [%s]. Meta: %s#%s.", p.getType(), method.getName(), p.getName()));
                    }
                    Object pVal = args[i];
                    if (pVal == null) {
                        throw new IllegalArgumentException(String.format("null DistributedLockParam is not allowed: [%s]. Meta: %s#%s.", p.getType(), method.getName(), p.getName()));
                    }
                    params.add(p.getType().isEnum() ? ((Enum<?>) pVal).name() : pVal);
                }
            }
            if (annotation.needParam() && params.isEmpty()) {
                return null;
            }
            CacheKeyAssembleHandler assembleHandler = applicationContext.getBean(annotation.assembleHandler());

            String paramAssembleKey = assembleHandler.assemble(params);
            String primaryKey;
            if (StringUtils.hasLength(annotation.primaryKey())) {
                primaryKey = annotation.primaryKey();
                return primaryKey + paramAssembleKey;
            }
            DistributedConstants.LockPrimaryKeyEnum primaryKeyEnum = annotation.value();
            primaryKey = primaryKeyEnum == DistributedConstants.LockPrimaryKeyEnum.DEFAULT ? method.getName() : primaryKeyEnum.name();
            return primaryKey + paramAssembleKey;
        } catch (Exception e) {
            log.error("", e);
        }
        return null;
    }

    private boolean checkIsAllowedType(Class<?> paramType) {
        if (paramType.isEnum()) {
            return true;
        }
        return ALLOWED_PARAM_KEY_TYPES.contains(paramType) || AssembleKeyParameter.class.isAssignableFrom(paramType);
    }
}
